package aceim.api.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import aceim.api.ICoreService;
import aceim.api.IProtocol;
import aceim.api.dataentity.Buddy;
import aceim.api.dataentity.BuddyGroup;
import aceim.api.dataentity.ConnectionState;
import aceim.api.dataentity.FileProgress;
import aceim.api.dataentity.InputFormFeature;
import aceim.api.dataentity.ItemAction;
import aceim.api.dataentity.Message;
import aceim.api.dataentity.MessageAckState;
import aceim.api.dataentity.OnlineInfo;
import aceim.api.dataentity.PersonalInfo;
import aceim.api.utils.Logger;
import aceim.api.utils.Logger.LoggerLevel;
import android.content.Context;
import android.os.DeadObjectException;
import android.os.RemoteException;
/**
* Account service base, contains all inner-account logic. Should be nested in protocol implementation.
*/
public abstract class AccountService {
/**
* Account service ID.
*/
private final byte serviceId;
/**
* Account protocol UID.
*/
private final String protocolUid;
/**
* AIDL core callback.
*/
private final ICoreProtocolCallback callback;
/**
* Android context.
*/
private final Context context;
private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
private final Map<String, OnlineInfo> onlineInfos = new ConcurrentHashMap<String, OnlineInfo>();
private ScheduledFuture<?> onlineInfoUpdater = null;
/**
* {@link ICoreService}-to-AIDL translator.
*/
private final ICoreService coreService = new ICoreService() {
@Override
public void typingNotification(String ownerUid) {
try {
callback.typingNotification(serviceId, ownerUid);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void showFeatureInputForm(String uid, InputFormFeature feature) {
try {
callback.showFeatureInputForm(serviceId, uid, feature);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void searchResult(List<PersonalInfo> infoList) {
try {
callback.searchResult(serviceId, infoList);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public String requestPreference(String preferenceName) {
try {
return callback.requestPreference(serviceId, preferenceName);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
return null;
}
@Override
public void personalInfo(PersonalInfo info, boolean isShortInfo) {
try {
callback.personalInfo(info, isShortInfo);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void notification(String message) {
try {
callback.notification(serviceId, message);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void multiChatParticipants(String chatUid, List<BuddyGroup> participantList) {
try {
callback.multiChatParticipants(serviceId, chatUid, participantList);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void messageAck(String ownerUid, long messageId, MessageAckState state) {
try {
callback.messageAck(serviceId, ownerUid, messageId, state);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void message(Message message) {
try {
callback.message(message);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void iconBitmap(String ownerUid, byte[] data, String hash) {
try {
callback.iconBitmap(serviceId, ownerUid, data, hash);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void groupAction(ItemAction action, BuddyGroup newGroup) {
try {
callback.groupAction(action, newGroup);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void fileProgress(FileProgress progress) {
try {
callback.fileProgress(progress);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void connectionStateChanged(ConnectionState connState, int extraParameter) {
try {
callback.connectionStateChanged(serviceId, connState, extraParameter);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void buddyStateChanged(List<OnlineInfo> infos) {
for (OnlineInfo info : infos) {
onlineInfos.put(info.getProtocolUid(), info);
}
if (onlineInfoUpdater == null) {
onlineInfoUpdater = scheduledExecutor.schedule(onlineInfosRunnable, 1, TimeUnit.SECONDS);
}
}
@Override
public void buddyListUpdated(List<BuddyGroup> buddyList) {
try {
callback.buddyListUpdated(serviceId, buddyList);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void buddyAction(ItemAction action, Buddy newBuddy) {
try {
callback.buddyAction(action, newBuddy);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void accountStateChanged(OnlineInfo info) {
try {
callback.accountStateChanged(info);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
@Override
public void accountActivity(String text) {
try {
callback.accountActivity(serviceId, text);
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
}
};
/**
* As new online infos may arrive too often, we cache them and make calls to core periodically.
*/
private final Runnable onlineInfosRunnable = new Runnable() {
@Override
public void run() {
try {
while(onlineInfos.size() > 0) {
callback.buddyStateChanged(new ArrayList<OnlineInfo>(onlineInfos.values()));
onlineInfos.clear();
}
} catch (RemoteException e) {
onRemoteException(e);
} catch (Exception e) {
Logger.log(e);
}
onlineInfoUpdater = null;
}
};
protected AccountService(byte serviceId, String protocolUid, ICoreProtocolCallback callback, Context context) {
this.serviceId = serviceId;
this.protocolUid = protocolUid;
this.callback = callback;
this.context = context;
}
private void onRemoteException(RemoteException e) {
if (e instanceof DeadObjectException) {
Logger.log("Callback is dead - shutting down");
} else {
Logger.log(e);
}
}
public abstract IProtocol getProtocol();
protected abstract void timeoutReconnect();
protected abstract ConnectionState getCurrentState();
/**
* @return the serviceId
*/
public byte getServiceId() {
return serviceId;
}
/**
* @return the protocolUid
*/
public String getProtocolUid() {
return protocolUid;
}
/**
* @return the coreService
*/
public ICoreService getCoreService() {
return coreService;
}
/**
* @return the context
*/
public Context getContext() {
return context;
}
/*
* The following code implements simple trying to override the "dead socket" issue (http://code.google.com/p/android/issues/detail?id=6144).
* It simply sends some message, which needs to be answered by server in some amount of time. Of course, different protocols have different
* ways to implement this logic, but in any case it should be some protocol message, that does not influence on overall client-server
* conversation logic, but do the responding. For example, ICQ has some deprecated or rarely used API calls with responding.
*/
public static final String PING_TIMEOUT = "pingtimeout";
private KeepaliveTimer keepaliveTimer = new KeepaliveTimer();
private ScheduledFuture<?> task;
private Runnable timeoutRunnable = new Runnable(){
@Override
public void run() {
if (getCurrentState() == ConnectionState.CONNECTED && keepaliveTimer.running){
keepaliveTimer.running = false;
try {
Logger.log(getProtocolUid() + " could not wait for heartbeat, disconnecting", LoggerLevel.DEBUG);
timeoutReconnect();
} catch (Exception e1) {
Logger.log(e1);
}
}
}
};
public long pingTimeout = 0;
public void resetHeartbeat() {
Logger.log(getProtocolUid() + " got heartbeat", LoggerLevel.DEBUG);
if (task!=null){
task.cancel(false);
}
if (pingTimeout < 1){
return;
}
keepaliveTimer.running = true;
task = scheduledExecutor.schedule(keepaliveTimer, pingTimeout , TimeUnit.SECONDS);
}
public void closeKeepaliveThread() {
resetHeartbeat();
}
private void schedule(){
if (pingTimeout < 1){
return;
}
Logger.log("schedule... "+getCurrentState(), LoggerLevel.DEBUG);
try {
if (getCurrentState() == ConnectionState.CONNECTED){
keepaliveRequest();
task = scheduledExecutor.schedule(timeoutRunnable, pingTimeout , TimeUnit.SECONDS);
keepaliveTimer.running = true;
Logger.log(getProtocolUid() + " sent heartbeat request", LoggerLevel.DEBUG);
}
} catch (Exception e) {
Logger.log(e);
}
}
protected abstract void keepaliveRequest();
public void sendKeepalive(){
if (pingTimeout > 0){
Logger.log("start keepalive "+getProtocolUid(), LoggerLevel.VERBOSE);
resetHeartbeat();
} else {
Logger.log("skip keepalive "+getProtocolUid(), LoggerLevel.VERBOSE);
}
}
class KeepaliveTimer extends Thread{
public volatile boolean running = true;
@Override
public void run(){
if (running){
if (task!=null){
task.cancel(false);
}
schedule();
}
}
}
}